package com.springboot.demo.io.socketmultiplex;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

import com.springboot.demo.io.Constants;

/**
 * NIO服务处理器
 *
 * @author supu
 * @date 2019-12-03 15:11
 **/
public class NIOServerHandler implements Runnable {
    // 多路复用器，NIO编程的基础，负责管理通道Channel
    private Selector selector;
    // 缓冲区buffer
    private ByteBuffer byteBuffer = ByteBuffer.allocate(Constants.BUFFER_SIZE);

    public NIOServerHandler(Selector selector) {
        this.selector = selector;
    }


    /**
     * 开启线程负责Selector轮询
     */
    @Override
    public void run() {
        while (true) {
            try {
                /**
                 * a.select() 阻塞到至少有一个通道在你注册的事件上就绪
                 * b.select(long timeOut) 阻塞到至少有一个通道在你注册的事件上就绪或者超时timeOut
                 * c.selectNow() 立即返回。如果没有就绪的通道则返回0
                 * select方法的返回值表示就绪通道的个数。
                 */
                // 1、多路复用器监听阻塞
                selector.select();
                System.out.println("阻塞了吗？");
                // 2、获取多路复用器已经选择的结果集
                Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
                // 3、不停地轮询所有的Channel
                while (selectionKeys.hasNext()) {
                    // 4、获取当前选中的key
                    SelectionKey key = selectionKeys.next();
                    // 5、获取后便将其从容器中移除
                    selectionKeys.remove();
                    if (!key.isValid()) {
                        // 只获取有效地key
                        continue;
                    }
                    // 新来的请求
                    if (key.isAcceptable()) {
                        accept(key);
                    }

                    // 可读状态处理
                    if (key.isReadable()) {
                        System.out.println("开始读取数据");
                        read(key);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void read(SelectionKey key) {
        try {
            // 1、清空缓冲区数据
            byteBuffer.clear();
            // 2、获取在 selector 上注册的channel
            SocketChannel socketChannel = (SocketChannel) key.channel();
            // 3、读取数据
            int index = socketChannel.read(byteBuffer);
            if (index == -1) {
                // -1 标示无任何数据
                socketChannel.close();
                key.cancel();
                return;
            }

            // 4、有数据则在读取数据前进行复位操作
            byteBuffer.flip();
            // 5、根据缓冲区大小创建一个相应大小的bytes数组，用来获取值
            byte[] bytes = new byte[byteBuffer.remaining()];
            // 6、接收缓冲区数据
            byteBuffer.get(bytes);
            String requestMsg = new String(bytes);
            System.out.println("NIO server received data:" + requestMsg);

            // 返回响应数据给客户端
            write(socketChannel, requestMsg + " is response");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 返回数据给客户端
     *
     * @param socketChannel
     * @param s
     */
    private void write(SocketChannel socketChannel, String s) {
        try {
            // 创建ByteBuffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(Constants.BUFFER_SIZE);
            // 将返回数据放入缓存区
            byteBuffer.put(s.getBytes());
            // 缓存区数据复位
            byteBuffer.flip();
            // 发送缓冲区数据
            socketChannel.write(byteBuffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 设置阻塞，等待Client请求。在传统IO编程中，用的是ServerSocket和Socket。在NIO中采用的ServerSocketChannel和SocketChannel
    private void accept(SelectionKey key) {
        try {
            // 1、获取通道服务
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            // 2、执行阻塞方法
            SocketChannel socketChannel = serverSocketChannel.accept();
            // 3、设置通道服务为非阻塞，true 为阻塞，false 为非阻塞
            socketChannel.configureBlocking(false);
            // 通道注册到 selector 上去，并设置读取标识
            socketChannel.register(selector, SelectionKey.OP_READ);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
